<?php
class XServer {
    public $port = 8080;
    public $pid = 0;
    private $protocol = 'tcp';
    private $ip = '0.0.0.0';
    private $sock = null;
    private $server = null;
    public $errno = 0;
    public $errstr = null;
    private $connect_pool = array();
    private $buffer_pool = array();
    private $scheduler = null;
    private $request_line_end = false;
    private $request_line_find = false;
    private $header_buff = '';
    private $request_body = '';
    private $response_status = '';
    private $lastest_header ='';
    private $web_worker_pool = array();
    public function __construct($scheduler) {
        global $_CFG;
        $this->scheduler = $scheduler;
        $this->check_system();
        $this->check_extension();
        if($_CFG->server->daemon) {
            $this->daemon();
        }
        define('__X_MASTER_PID__', posix_getpid());
        $this->setproctitle('XPHPFramework-web-master');
        $this->run_web();
    }
    public function check_system() {
        if(strtoupper(substr(PHP_OS,0,3) === 'WIN')) {
            throw new XException('XServer class not support windows');
        }
        if(PHP_SAPI != 'cli') {
            throw new XException('XServer class only runing in php cli mode');
        }
    }
    public function check_extension() {
        $extension_suffix = strtoupper(substr(PHP_OS,0,3)) === 'WIN' ? '.dll':'.so';
        if(!extension_loaded('pcntl')) {
            if(!dl("pcntl{$extension_suffix}")) throw new XException('can not load pcntl extension');
        }
        if(!function_exists('pcntl_fork')) throw XException('pcntl extension function can not used');
        if(!extension_loaded('proctitle')) {
            if(!dl("proctitle{$extension_suffix}")) throw new XException('can not load proctitle extension');
        }
        if(!function_exists('setproctitle')) throw new XException('proctitle extension function can not used');
        if(!extension_loaded('posix')) {
            if(!dl("posix{$extension_suffix}")) throw new XException('can not load POSIX extension');
        }
        if(!function_exists('posix_getpid')) throw new XException('posix extension function can not used');
        if(!extension_loaded('libevent')) {
            if(!dl("libevent{$extension_suffix}")) throw new XException('can not load libevent extension');
        }
        if(!function_exists('event_base_new')) throw new XException('libevent extension function can not used');
    }
    public function setproctitle($name) {
        setproctitle($name);
    }
    public function get_loacl_socket() {
        return "{$this->protocol}://{$this->ip}:{$this->port}";
    }
    public function run_web() {
        $local_socket = $this->get_loacl_socket();
        $this->server = stream_socket_server($local_socket,$this->errno, $this->errstr);
        if(!$this->server) throw new XException($this->errstr);
        stream_set_blocking($this->server,0);
        $this->run_web_worker();
    }
    public function run_web_worker() {
        $base_evt = event_base_new();
        $evt = event_new();
        event_set($evt, $this->server, EV_READ|EV_PERSIST, array($this,'web_accept'), $base_evt);
        event_base_set($evt,$base_evt);
        event_add($evt);
        event_base_loop($base_evt);
    }
    public function fork_web_worker() {
        global $_CFG;
        for($i=0;$i<$_CFG->server->min_worker_num;$i++) {
            $pid = pcntl_fork();
            if($pid == -1) throw new XException('Fork web worker process error');
            if($pid > 0) {
                $this->web_worker_pool[$pid] = $pid;
                continue;
            }
            $this->setproctitle('XPHPFramework-web-worker');
            $this->run_web_worker();
            return;
        }
        while(true) {
            $pid = pcntl_wait($status);
            unset($this->web_worker_pool[$pid]);

        }
    }
    public function web_accept($sock,$flag,$base_evt) {
        $connect = stream_socket_accept($sock);
        stream_set_blocking($connect,0);
        $id = count($this->connect_pool) +1;
        $buff = event_buffer_new($connect, array($this,'web_read'),array($this,'web_write'),array($this,'web_error'), $id);
        event_buffer_base_set($buff, $base_evt);
        event_buffer_timeout_set($buff, 30,30);
        event_buffer_watermark_set($buff, EV_READ, 0, 0xffffff);
        event_buffer_priority_set($buff, 10);
        event_buffer_enable($buff, EV_READ|EV_PERSIST);
        $this->connect_pool[$id] = $connect;
        $this->buffer_pool[$id] = $buff;
    }
    public function web_error($buffer,$error,$id) {
        event_buffer_disable($this->buffer_pool[$id], EV_READ|EV_WRITE);
        event_buffer_free($this->buffer_pool[$id]);
        fclose($this->connect_pool[$id]);
        unset($this->connect_pool[$id],$this->buffer_pool[$id]);
    }
    public function web_write($buffer, $id) {
        echo 'write';
    }
    public function web_read($buffer, $id) {
        echo "read\n";
        $request_str = '';
        $_SERVER['SERVER_ADDR'] = gethostbyname(gethostname());
        $this->return_server_status('200');
        $this->get_request_header($buffer);
        $this->get_request_body($buffer);
        $this->scheduler->load_app();
        //$html_str = ob_get_contents();
        $html_str = '';
        $response_header = $this->get_response_header();
        $response_header .= $this->get_php_header();
        $response_header .= $this->set_length(strlen($html_str));
        $response_header .= "\r\n";
        $out = $response_header . ob_get_contents();
        event_buffer_write($buffer,$out);
    }
    public function set_length($len) {
        return "Content-Length:$len\r\n";
    }
    public function get_request_body($buffer) {
        while($read = event_buffer_read($buffer,1024)) {
            $this->request_body .= $buffer;
        }
    }
    public function get_request_header($buffer) {
        $this->header_buff = '';
        while($read = event_buffer_read($buffer,1024)) {
            $offset = strpos($this->header_buff, "\r\n\r\n");
            if($offset > 1) {
                $this->request_body = substr($this->header_buff, $offset+4);
                break;
            }
            $this->header_buff .= $read;
        }
        $header_field_list = explode("\r\n",$this->header_buff);
        $this->get_request_start_line($header_field_list[0]);
        foreach($header_field_list as $header) {
            $field = explode(':',$header);
            if(isset($field[0]) && trim($field[0]) == 'Host' && isset($field[1])) {
                $_SERVER['HTTP_HOST'] = $field[1];
            }
        }
    }
    public function get_request_start_line($read) {
        $this->header_buff .= $read;
        if(($crlf_index = strpos($this->header_buff,"\r\n")) !== false) {
            $request_line = substr($read,0,$crlf_index);
            $this->header_buff = substr($read,$crlf_index);
            $this->request_line_find = true;
            $uri_list = explode(' ', $request_line);
            if($uri_list[0] != 'POST' && $uri_list[0] != 'GET') {
                return $this->return_server_status('405');
            }
            $_SERVER['REQUEST_METHOD'] = $uri_list[0];
            if(empty($uri_list[1])) $uri_list[1] = '/';
            $_SERVER['REQUEST_URI'] = urldecode($uri_list[1]);
        } else {
            $this->header_buff .= $read;
        }
    }
    public function return_server_status($code) {
        $this->response_status = 'HTTP/1.1 ';
        switch($code) {
        case '405':
            $this->response_status .= '405 Method Not Allowed';
        break;
        case '404':
            $this->response_status .= '404 Not Found';
        break;
        case '403':
            $this->response_status .= '403 Forbidden';
        break;
        case '500':
            $this->response_status .= '500 Internal Server Error';
        break;
        case '301':
            $this->response_status .= '301 Moved Permanently';
        break;
        case '302':
            $this->response_status .= '302 Found';
        break;
        default:
            $this->response_status .= '200 OK';
        break;
        }
        $this->response_status .= "\r\n";
    }
    /**
     * RFC2616 set HTTP/1.1 header
     */
    public function get_response_header() {
        $header = $this->response_status;
        $header .= "Cache-Control:no-cache\r\n";
        $header .= "Connection:close\r\n";
        $header .= "Pragma:no-cache\r\n";
        $gdate = $this->set_server_date(gtime());
        $header .= "Date:{$gdate} GMT\r\n";
        $header .= "Content-Language:zh\r\n";
        $header .= "Content-Type:text/html;charset=utf-8\r\n";
        $header .= "Server:XPHPFramework\r\n";
        return $header;
    }
    public function set_server_date($time) {
        return gmdate('D, d M Y H:i:s', $time);
    }
    public function get_php_header() {
        global $_X_HTTP_HEADER;
        $str = '';
        if(is_array($_X_HTTP_HEADER)) {
            foreach($_X_HTTP_HEADER as $hstr) {
                $field = explode(':',trim($hstr));
                if(isset($field[0]) && trim($field[0]) == 'Location') {
                    $this->return_server_status(302);
                }
                $str .="$hstr\r\n";
            }
        }
        $str .= $this->get_setcookie_header();
        return $str;
    }
    /**
     * RFC6265 set cookie
     */
    public function get_setcookie_header() {
        global $_X_APP_RUNING,$_CFG;
        $header = '';
        if($sid = session_id()) {
            $header .= "{$_CFG->session->session_name}={$sid};";
        }
        if(!is_object($_X_APP_RUNING)) return '';
        if($_X_APP_RUNING->app_instance->R->C->num>0) {
            foreach($_X_APP_RUNING->app_instance->R->C as $cookie) {
                $cookie_name = urlencode($cookie->name);
                $cookie_value = urlencode($cookie_name);
                $header .= "{$cookie_name}={$cookie_value};";
                if($cookie->expire > 0) {
                    $date = $this->get_server_date(gtime() + $cookie->expire);
                    $header .= "Expires={$date};";
                }
                if(!empty($cookie->domain)) {
                    $header .= "Domain={$cookie->domain};";
                }
                $header .= "Path={$cookie->path};";
                if($cookie->secure) $header .="Secure;";
                if($cookie->httponly) $header .= "HttpOnly;";
            }
        }
        if(!empty($header)) {
            $header = "Set-Cookie:{$header}\r\n";
        }
        return $header;
    }
    
    public function daemon() {
        $fock_pid = pcntl_fork();
        if($fock_pid == -1) throw new XException('fork #1 Error');
        if($fock_pid >0) die;
        $fock_pid = pcntl_fork();
        if($fock_pid == -1) throw new XException('fork #2 ERROR');
        if($fock_pid>0) die;
        chdir('/');
        umask('0');
        posix_setsid();
        $sub_pid = pcntl_fork();
        if($sub_pid == -1) throw new XException('fork #3 ERROR');
        if($sub_pid >0) die;
        $this->pid = posix_getpid();
    }
}
